use std::sync::Arc;
use std::time::Duration;

use anyhow::bail;
use anyhow::Result;
use config::{Config, Map, Value};
use glob::Pattern;
use reqwest::header;
use reqwest::header::HeaderValue;
use reqwest::Client;
use reqwest::ClientBuilder;

use crate::cli::Cli;
use crate::default::VAR_BASEARCH;
use crate::default::VAR_RELEASEVER;

use super::config_main::ConfigMain;
use super::config_main::RdnfConfig;
use super::parse_string_to_vec;

#[derive(Debug, Default)]
pub struct RepoConfig {
    pub name: Arc<String>,
    pub full_name: String,
    pub cache_name: Option<String>,
    pub base: RepoConfigBase,
    pub detail: RepoConfigDetail,
}
#[derive(Debug)]
pub struct RepoConfigBase {
    pub enabled: bool,
    pub skip_if_unavailable: bool,
    pub gpgcheck: bool,
    pub priority: i64,
    pub timeout: u64,
    pub retries: i64,
    pub minrate: u64,
    pub throttle: u64,
    pub sslverify: bool,
    pub lmetadata_expire: i128,
    pub skip_md_filelists: bool,
    pub skip_md_updateinfo: bool,
    pub skip_md_other: bool,
}
#[derive(Debug, Default)]
pub struct RepoConfigDetail {
    pub base_url: Option<String>,
    pub meta_link: Option<String>,
    pub url_gpg_keys: Option<Vec<String>>,
    pub username: Option<String>,
    pub password: Option<String>,
    pub ssl_ca_cert: Option<String>,
    pub ssl_client_cert: Option<String>,
    pub ssl_client_key: Option<String>,
}
impl Default for RepoConfigBase {
    fn default() -> Self {
        Self {
            enabled: true,
            skip_if_unavailable: false,
            gpgcheck: true,
            sslverify: true,
            lmetadata_expire: 172800,
            priority: 50,
            timeout: 0,
            minrate: 0,
            throttle: 0,
            retries: 10,
            skip_md_filelists: false,
            skip_md_updateinfo: false,
            skip_md_other: false,
        }
    }
}
pub enum RepoListFilter {
    All,
    Enabled,
    Disabled,
}
impl RepoConfig {
    fn create(name: String) -> Self {
        Self {
            name: Arc::new(name.clone()),
            full_name: name,
            cache_name: Default::default(),
            base: RepoConfigBase::default(),
            detail: RepoConfigDetail::default(),
        }
    }
    pub fn read_all(repo_dir: &str) -> Result<Vec<Self>> {
        let pattern = repo_dir.trim_end_matches("/").to_string() + "/*.repo";
        let source = glob::glob(&pattern)?
            .map(|p| config::File::from(p.unwrap()).format(config::FileFormat::Ini))
            .collect::<Vec<_>>();
        let c = Config::builder().add_source(source).build()?;
        let mut repos = Vec::new();
        for (name, table) in c.try_deserialize::<Map<String, Value>>()? {
            let mut repo = RepoConfig::create(name);
            for (key, value) in table.into_table()? {
                if let Some(e) = repo.update_repo(key.as_str(), value).err() {
                    bail!(
                        "repo {} : option {} : {}",
                        repo.name.as_str(),
                        key.as_str(),
                        e
                    )
                }
            }
            repos.push(repo);
        }
        repos.sort_by(|a, b| a.base.priority.cmp(&b.base.priority));
        repos.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(repos)
    }
    pub fn update_repo(&mut self, key: &str, v: config::Value) -> Result<()> {
        match key {
            "enabled" => self.base.enabled = v.into_bool()?,

            "name" => self.full_name = v.into_string()?,

            "baseurl" => {
                self.detail.base_url = Some(v.into_string()?.trim_end_matches("/").to_owned() + "/")
            }

            "metalink" => self.detail.meta_link = Some(v.into_string()?),

            "skip_if_unavailable" => self.base.skip_if_unavailable = v.into_bool()?,

            "gpgcheck" => self.base.gpgcheck = v.into_bool()?,

            "gpgkey" => self.detail.url_gpg_keys = parse_string_to_vec(v.into_string()?.as_str()),

            "username" => self.detail.username = Some(v.into_string()?),

            "password" => self.detail.password = Some(v.into_string()?),

            "priority" => self.base.priority = v.into_int()?,

            "timeout" => self.base.timeout = v.into_uint()?,

            "retries" => self.base.retries = v.into_int()?,

            "minrate" => self.base.minrate = v.into_uint()?,

            "throttle" => self.base.throttle = v.into_uint()?,

            "sslverify" => self.base.sslverify = v.into_bool()?,

            "sslcacert" => self.detail.ssl_ca_cert = Some(v.into_string()?),

            "sslclientcert" => self.detail.ssl_client_cert = Some(v.into_string()?),

            "sslclientkey" => self.detail.ssl_client_key = Some(v.into_string()?),

            "metadata_expire" => {
                let s = v.into_string()?;
                if s == "never" {
                    self.base.lmetadata_expire = -1;
                } else {
                    self.base.lmetadata_expire = match s.parse::<i128>() {
                        Ok(t) => t,
                        Err(_) => {
                            let (num, mul) = s.split_at(s.len() - 1);
                            let n = match num.parse::<i128>() {
                                Ok(n) => n,
                                Err(_) => {
                                    bail!("should be like 1 or 1d or 1h or 1m or 1s")
                                }
                            };
                            match mul {
                                "s" => n,
                                "m" => 60 * n,
                                "h" => 60 * 60 * n,
                                "d" => 60 * 60 * 24 * n,
                                _ => {
                                    bail!("the unit of time should be d,h,m,s(default)")
                                }
                            }
                        }
                    }
                }
            }

            "skip_md_filelists" => self.base.skip_md_filelists = v.into_bool()?,

            "skip_md_updateinfo" => self.base.skip_md_updateinfo = v.into_bool()?,

            "skip_md_other" => self.base.skip_md_other = v.into_bool()?,

            _ => {}
        }
        Ok(())
    }
    fn update_cache_name(&mut self, url: &str) {
        let context = self.name.to_string() + url;
        let s = uuid::Builder::from_bytes(md5::compute(context).0)
            .into_uuid()
            .to_string();
        let (a, b) = s.split_once("-").unwrap();
        self.cache_name = Some(self.name.to_string() + "-" + a);
    }
}
fn alter_repo_state_enable(repos: &mut Vec<RepoConfig>, enable: bool, pattern: &str) -> Result<()> {
    match Pattern::new(pattern) {
        Ok(p) => {
            for repo in &mut *repos {
                if p.matches(&repo.name) {
                    repo.base.enabled = enable;
                }
            }
        }
        Err(e) => {
            bail!("Failed to enablerepo {}, because {}", &pattern, e)
        }
    }
    Ok(())
}
pub fn finalize_repos(cli: &Cli, config: &RdnfConfig, repos: &mut Vec<RepoConfig>) -> Result<()> {
    if let Some(ref v) = cli.repoid {
        alter_repo_state_enable(repos, false, "*")?;
        for pattern in v {
            alter_repo_state_enable(repos, true, pattern)?;
        }
    }
    if let Some(ref v) = cli.enablerepo {
        for pattern in v {
            alter_repo_state_enable(repos, true, pattern)?;
        }
    }
    if let Some(ref v) = cli.disablerepo {
        for pattern in v {
            alter_repo_state_enable(repos, false, pattern)?;
        }
    }
    for ele in repos {
        ele.full_name = ele
            .full_name
            .replace(VAR_RELEASEVER, &config.var_release_ver)
            .replace(VAR_BASEARCH, &config.var_base_arch);

        match &ele.detail.meta_link {
            Some(s) => {
                let meta_link = s
                    .replace(VAR_RELEASEVER, &config.var_release_ver)
                    .replace(VAR_BASEARCH, &config.var_base_arch);
                ele.update_cache_name(&meta_link);
                ele.detail.meta_link = Some(meta_link);
            }
            None => match &ele.detail.base_url {
                Some(url) => {
                    ele.update_cache_name(url.clone().as_str());
                }
                None => {
                    bail!(
                        "Repo {} doesn't have base_url or meta_link ; please check",
                        ele.name
                    );
                }
            },
        }
        if let Some(ref s) = ele.detail.url_gpg_keys {
            let r = s
                .iter()
                .map(|x| {
                    x.replace(VAR_RELEASEVER, &config.var_release_ver)
                        .replace(VAR_BASEARCH, &config.var_base_arch)
                })
                .collect::<Vec<_>>();
            ele.detail.url_gpg_keys = if r.len() > 0 { Some(r) } else { None }
        }
    }
    Ok(())
}
impl RepoConfig {
    pub fn get_req_client(&self, config: &ConfigMain) -> Result<Client> {
        let mut headers = header::HeaderMap::new();
        if let Some(username) = &self.detail.username {
            headers.insert(
                header::AUTHORIZATION,
                basic_auth(username, self.detail.password.clone()),
            );
        }
        if let Some(username) = &config.proxy_username {
            headers.insert(
                header::PROXY_AUTHORIZATION,
                basic_auth(username, config.proxy_password.clone()),
            );
        }
        let mut client_builder = ClientBuilder::new()
            .user_agent("rdnf")
            .default_headers(headers);
        if self.base.timeout != 0 {
            client_builder = client_builder.timeout(Duration::from_secs(self.base.timeout));
        }
        Ok(client_builder.build()?)
    }
}
fn basic_auth<U, P>(username: U, password: Option<P>) -> HeaderValue
where
    U: std::fmt::Display,
    P: std::fmt::Display,
{
    use base64::prelude::BASE64_STANDARD;
    use base64::write::EncoderWriter;
    use std::io::Write;

    let mut buf = b"Basic ".to_vec();
    {
        let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
        let _ = write!(encoder, "{}:", username);
        if let Some(password) = password {
            let _ = write!(encoder, "{}", password);
        }
    }
    let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
    header.set_sensitive(true);
    header
}
#[cfg(test)]
mod tests {
    use super::RepoConfig;
    use anyhow::Result;
    const REPO_DIR: &str = "assest/repos.d";
    #[test]
    fn test_read() -> Result<()> {
        let repos = RepoConfig::read_all(REPO_DIR)?;
        assert_eq!(repos.len(), 2);
        Ok(())
    }
}
